package com.babel.util.redis;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
 * 分布式锁
 * @author chenhui
 *
 */
public class DistributedRedislock {
	
    private StringRedisTemplate stringRedisTemplate;
	
	/**
	* 锁超时时间，防止线程在入锁以后，无限的执行等待
	*/
    private int expireMsecs = 10 * 2000;
    
    /**
     * 是否使用延时模式,延时模式下锁超时设置不生效（正常情况下）。
     */
    private boolean isExpire = true;
    /**
     * 延长锁时间
     */
    private int addExpireMsecs = 10 * 100;
    /**
     * 锁等待时间，防止线程饥饿
     */
    private int timeoutMsecs = 10 * 1000;
    
 //   private volatile static int num = 0;
    
    //本地线程存入时间戳，控制是否删除（当超时但是线程没死时，日后优化成回调函数控制超时时间）
    private static final Map<Long, String> contextHolder = new HashMap<Long, String>();
    
    private static final Map<String, Thread> mapThread=new HashMap<String, Thread>();
    
    //锁再次获取时间
    private int resolution_millis = 100;
    
    private static Random random = new Random();
    
    /**
     * Lock key path.
     */
    private String lockKey;
    //uuid和时间戳的间隔
    private static final String timestr = "**time**";
    
    private volatile boolean locked = false;
    
    /**
     * 使用默认配置的锁
     * 
     * 不启用延时
     * @param stringRedisTemplate
     * @param lockKey
     */
    public DistributedRedislock(StringRedisTemplate stringRedisTemplate, String lockKey) {
    	this.stringRedisTemplate = stringRedisTemplate;
    	this.lockKey = lockKey + "_lock";
    	this.isExpire = false;
    }
    
    /**
     * 使用默认配置的锁
     * @param stringRedisTemplate
     * @param lockKey
     * @param isExpire 是否启用延时
     */
    public DistributedRedislock(StringRedisTemplate stringRedisTemplate, String lockKey, boolean isExpire) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockKey = lockKey + "_lock";
        this.isExpire = isExpire;
    }
    /**
     * 自定义配置锁(跟具业务来配置)
     * @param stringRedisTemplate
     * @param lockKey
     */
    public DistributedRedislock(StringRedisTemplate stringRedisTemplate, String lockKey, int expireMsecs, int addExpireMsecs, int timeoutMsecs, int resolution_millis, boolean isExpire) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.lockKey = lockKey + "_lock";
        this.expireMsecs=expireMsecs;
        this.addExpireMsecs=addExpireMsecs;
        this.timeoutMsecs=timeoutMsecs;
        this.resolution_millis=resolution_millis;
        this.isExpire=isExpire;
    }
   
    /**
     * @return lock key
     */
    public String getLockKey() {
        return lockKey;
    }
    /**
     * 没有值则添加后返回true,否则返回false
     * @param key
     * @param value
     * @return
     */
    private boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
        	e.printStackTrace();
            System.out.println("setNX redis error, key : {}" + key);
        }
        return obj != null ? (Boolean) obj : false;
    }
    /**
     * 先用新值覆盖旧值，在返回旧值,只有当锁超时时才会走此方法
     * @param key
     * @param value
     * @return
     */
    private String getSet(final String key, final String value) {
    	//System.out.println("进入getset-------------锁超时了");
        Object obj = null;
        try {
            obj = stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(ret);
                }
            });
        } catch (Exception e) {
        	e.printStackTrace();
        	System.out.println("getSet redis error, key : {}" + key);
        }
        return obj != null ? (String) obj : null;
    }
    
    private String get(final String key) {
        Object obj = null;
        try {
            obj = stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.get(serializer.serialize(key));
                    connection.close();
                    if (data == null) {
                        return null;
                    }
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            System.out.println("get redis error, key : {}" + key);
        }
        return obj != null ? obj.toString() : null;
    }
    private void set(final String key, final String value) {
        Object obj = null;
        try {
            obj = stringRedisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    connection.set(serializer.serialize(key),serializer.serialize(value));
                    connection.close();
                    return null;
                }
            });
        } catch (Exception e) {
            System.out.println("set redis error, key : {}" + key);
        }
    }
    
    /**
     * 获得 lock.
     * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
     * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
     * 执行过程:
     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
     * 
     * 
     * 关于同步的问题，并发访问量不是很高的时候，不需要同步，当并发量访问量很高的时候，激烈的竞争会拉低redis的性能。需要加上数据锁，控制对redis的访问。
     * 锁定的数量不能太低，否则会造成redis竞争的不公平
     * synchronized
     */
    public  boolean lock() throws InterruptedException {
    	System.out.println(Thread.currentThread().getId()+"lock我来了");
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
        	System.out.println(Thread.currentThread().getId()+"lock我来试着获取下");
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = UUID.randomUUID().toString() + timestr + String.valueOf(expires); //锁到期时间
          //  System.out.println(lockKey);
            if (this.setNX(lockKey, expiresStr)) {
            	contextHolder.put(Thread.currentThread().getId(),expiresStr);//加入本地线程
            	System.out.println(Thread.currentThread().getId()+"正常获取");
                // lock acquired
                locked = true;
                if(isExpire){
                	delayStart();//启动自动延时
                }
                return true;
            }

            String currentValueStr = this.get(lockKey); //redis里的时间
            String currentValueTimeStr = null;
            if(currentValueStr != null){
            	currentValueTimeStr = currentValueStr.substring(currentValueStr.indexOf(timestr) + timestr.length());
            }
            System.out.println(Thread.currentThread().getId()+"redis时间戳："+currentValueTimeStr);
            System.out.println(Thread.currentThread().getId()+"sys时间戳："+System.currentTimeMillis());
            if (currentValueTimeStr != null && Long.parseLong(currentValueTimeStr) < System.currentTimeMillis()) {
                //判断是否为空，不为空的情况下，如果被其他线程设置了值，则第二个条件判断是过不去的
                // lock is expired

                String oldValueStr = this.getSet(lockKey, expiresStr);
                //System.out.println(Thread.currentThread().getId()+"-----超时----" + num++);
                //获取上一个锁到期时间，并设置现在的锁到期时间，
                //只有一个线程才能获取上一个线上的设置时间，因为jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                	contextHolder.put(Thread.currentThread().getId(), expiresStr);//加入本地线程
                    //防止误删（覆盖，因为key是相同的）了他人的锁——这里达不到效果，这里值会被覆盖，但是因为什么相差了很少的时间，所以可以接受

                    //[分布式的情况下]:如过这个时候，多个线程恰好都到了这里，但是只有一个线程的设置值和当前值相同，他才有权利获取锁
                    // lock acquired
                    locked = true;
                    if(isExpire){
                    	delayStart();//启动自动延时
                    }
                    return true;
                }
            }
            int sleepMillis = random.nextInt(resolution_millis);
            timeout -= sleepMillis;

            
		      /* 延迟100 毫秒,  这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
		                只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
		                使用随机的等待时间可以一定程度上保证公平性*/
            Thread.sleep(sleepMillis);
        }
        System.out.println("没获取到");
        return false;
    }
    
    /**
     * Acqurired lock release.
     */
    public void unlock() {
        if (locked) {
        	try {
        		if(isExpire){
        			delayStop();//停止自动延时
                }
        		String expirestr = contextHolder.get(Thread.currentThread().getId());//加入本地线程
        		String oldValueStr = this.get(lockKey);
        		if(expirestr.equals(oldValueStr)){
        			stringRedisTemplate.delete(lockKey);
        			System.out.println(Thread.currentThread().getId()+"解锁成功");
        			locked = false;
        		}else{
        			locked = false;
        			//抛出异常回滚
        		}
        	//	System.out.println("解锁失败");
			} catch (Exception e) {
				e.printStackTrace();
				System.out.println("解锁失败");
			}finally{
				try {
					if(locked){
						stringRedisTemplate.delete(lockKey);
		                locked = false;
		                System.out.println("再次尝试解锁");
					}
				} catch (Exception e2) {
					System.out.println("二次失败，不再尝试");
				}
			}
        	
        }
    }
    
    private void delayStart(){
    	DelayThread t=(DelayThread)DistributedRedislock.mapThread.get(this.lockKey);
    	if(t == null){
    		DistributedRedislock.mapThread.put(this.lockKey, new DelayThread());
    		t = (DelayThread)DistributedRedislock.mapThread.get(this.lockKey);
    		t.setLocalThreadId(Thread.currentThread().getId());
    		t.start();
    	}else{
    		if(!t.getFlag()){
    			t.setLocalThreadId(Thread.currentThread().getId());
    			t.setFlag(true);//开启延时
    		}
    	}
    }
    private void delayStop(){
    	DelayThread t=(DelayThread)DistributedRedislock.mapThread.get(this.lockKey);
    	if(t != null && t.getLocalThreadId() == Thread.currentThread().getId()){
    		t.setFlag(false);
    	}
    }
    //内部类构造延时通知线程。每个lockkey启动一个线程
    class DelayThread extends Thread{

		private volatile boolean flag=true;
		private volatile long localThreadId  ;//验证锁是否被本地线程抢走，如果被本地线程抢走则不关闭延时通知
		@Override
		public void run(){
			System.out.println("延时器开启");
			while(true){
				try {
					Thread.sleep(addExpireMsecs);
					if(flag){
						long expires = System.currentTimeMillis() + expireMsecs + 1;
			           //////////////////----------------
						String expiresStr = UUID.randomUUID().toString() + timestr + String.valueOf(expires); //锁到期时间
			            set(lockKey,expiresStr);
			            contextHolder.put(localThreadId, expiresStr);
			            System.out.println(localThreadId+"延时至："+expiresStr);
					}else{
						System.out.println("不延时");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		public void setFlag(boolean flag){
			this.flag=flag;
		}
		public boolean getFlag(){
			return this.flag;
		}
		public long getLocalThreadId() {
			return localThreadId;
		}
		public void setLocalThreadId(long localThreadId) {
			this.localThreadId = localThreadId;
		}
		
		
	
    }
    
    
}
